﻿using Sidvall.AuthorizationManager.Net.Services.V1;
using Sidvall.Security;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;

namespace Sidvall.AuthorizationManager.Data
{
	public class AuthorizationScopeItem
	{
		#region Public Members

		public long AuthorizationScopeId { get; set; }
		public AuthorizationScopeAccessLevel DefaultResourceActionAccessLevel { get; set; }
		public AuthorizationScopeAccessLevel DefaultResourceActionItemAccessLevel { get; set; }
		public ResourceItemActionPolicy ResourceItemActionPolicy { get; set; }
		public MissingUserPolicy MissingUserPolicy { get; set; }
		public string DefaultUserContext { get; set; }
		public bool CacheResourceItemActions { get; set; }
		public bool CacheUserItems { get; set; }
		public bool RequireActiveUser { get; set; }
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
		public ResourceItemCollectionDictionary ResourceActions { get; set; }
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
		public ResourceItemCollectionDictionary ResourceItemActions { get; set; }
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
		public Sidvall.Security.UserItemCollection UserItems { get; set; }

		#region GetSystemResourceItemsAsync

		public Task<Sidvall.Security.ResourceItemCollection> GetSystemResourceItemsAsync()
		{
			var items = new Sidvall.Security.ResourceItemCollection();
			if (this.ResourceActions != null)
			{
				foreach (var resourceItems in this.ResourceActions)
				{
					foreach (var resourceItem in resourceItems.Value)
						items.Add(resourceItems.Key, resourceItem.Action, resourceItem.SubAction, null, resourceItem.GetDescription(),
							GetResourceItemAccessLevel(resourceItem));
				}
			}
			switch (this.DefaultResourceActionAccessLevel)
			{
				case AuthorizationScopeAccessLevel.Public:
					if (!items.Contains(string.Empty))
						items.Add(null, null, null, null, null, ResourceItemAccessLevel.Public);
					break;
				case AuthorizationScopeAccessLevel.Authenticated:
					if (!items.Contains(string.Empty))
						items.Add(null, null, null, null, "Authenticated", ResourceItemAccessLevel.Protected);
					break;
				default:
					break;
			}
			return Task.FromResult(items);
		}

		#endregion
		#region GetUserResourceItemsAsync

		public async Task<Sidvall.Security.ResourceItemCollection> GetUserResourceItemsAsync(IPrincipal principal, UserValidationResult userValidationResult)
		{
			var items = new Sidvall.Security.ResourceItemCollection();
			var userItem = await GetUserItemAsync(userValidationResult, null, false, principal).ConfigureAwait(false);
			var userIsValid = IsValid(userItem);
			if (this.ResourceActions != null)
			{
				foreach (var resourceItem in this.ResourceActions)
					AuthorizationScopeItemManager.LoadResourceItems(items, resourceItem.Key, resourceItem.Value, principal, userIsValid);
			}
			switch (this.DefaultResourceActionAccessLevel)
			{
				case AuthorizationScopeAccessLevel.Public:
					if (!items.Contains(string.Empty))
						items.Add(null, null, null, null, null, ResourceItemAccessLevel.Public);
					break;
				case AuthorizationScopeAccessLevel.Authenticated:
					if ((principal != null) && (principal.Identity != null) && (principal.Identity.IsAuthenticated) && (userIsValid))
					{
						if (!items.Contains(string.Empty))
							items.Add(null, null, null, null, "Authenticated", ResourceItemAccessLevel.Protected);
					}
					break;
				default:
					break;
			}
			return items;
		}

		#endregion
		#region GetUserItemAsync

		public async Task<Sidvall.Security.UserItem> GetUserItemAsync(string name, UserValidationResult userValidationResult)
		{
			var userItem = await GetUserItemAsync(userValidationResult, null, true, name).ConfigureAwait(false);
			AddUserItemToCache(userItem);
			return userItem;
		}

		#endregion
		#region IsAuthorized

		public bool IsAuthorized(IPrincipal principal, Sidvall.Security.ResourceItemCollection items)
		{
			if (items == null)
				throw new System.ArgumentNullException("items");
			var userItem = GetUserItemFromCache(principal);
			return IsAuthorized(principal, items, userItem);
		}
		private bool IsAuthorized(IPrincipal principal, Sidvall.Security.ResourceItemCollection items, UserItem userItem)
		{
			bool result = items.Count > 0;
			foreach (var item in items)
			{
				if (!IsAuthorized(principal, item, userItem))
					result = false;
			}
			return result;
		}
		private bool IsAuthorized(IPrincipal principal, Sidvall.Security.ResourceItem resourceItem, UserItem userItem)
		{
			var item = GetResourceItem(resourceItem.Resource, resourceItem.Action, resourceItem.SubAction, false);
			var result = IsAuthorized(principal, IsValid(userItem), item, resourceItem, this.DefaultResourceActionAccessLevel);
			return result;
		}
		private static bool IsAuthorized(IPrincipal principal, bool userIsValid, ResourceItem item, Sidvall.Security.ResourceItem resourceItem,
			AuthorizationScopeAccessLevel defaultAccessLevel)
		{
			if (item != null)
			{
				return item.IsAuthorized(principal, userIsValid, resourceItem);
			}
			else
			{
				switch (defaultAccessLevel)
				{
					case AuthorizationScopeAccessLevel.Public:
						resourceItem.AccessLevel = ResourceItemAccessLevel.Public;
						return true;
					case AuthorizationScopeAccessLevel.Authenticated:
						if ((principal == null) || (principal.Identity == null) || (!principal.Identity.IsAuthenticated) || (!userIsValid))
						{
							resourceItem.AccessLevel = ResourceItemAccessLevel.None;
							return false;
						}
						resourceItem.AccessLevel = ResourceItemAccessLevel.Protected;
						return true;
					default:
						resourceItem.AccessLevel = ResourceItemAccessLevel.None;
						return false;
				}
			}
		}

		#endregion
		#region IsAuthorizedAsync

		public async Task<bool> IsAuthorizedAsync(IPrincipal principal, UserValidationResult userValidationResult, Sidvall.Security.ResourceItemCollection items)
		{
			if (items == null)
				throw new System.ArgumentNullException("items");
			var userItem = await GetUserItemAsync(userValidationResult, null, false, principal).ConfigureAwait(false);
			bool result = items.Count > 0;
			foreach (var item in items)
			{
				if (string.IsNullOrWhiteSpace(item.ResourceItemId))
				{
					if (!IsAuthorized(principal, item, userItem))
						result = false;
				}
				else
				{
					if (!await IsAuthorizedAsync(principal, item, userItem).ConfigureAwait(false))
						return false;
				}
			}
			return result;
		}
		private async Task<bool> IsAuthorizedAsync(IPrincipal principal, Sidvall.Security.ResourceItem resourceItem, UserItem userItem)
		{
			ResourceItem item;

			switch (this.ResourceItemActionPolicy)
			{
				case ResourceItemActionPolicy.UseActions:
					item = GetResourceItem(resourceItem.Resource, resourceItem.Action, resourceItem.SubAction, false);
					return IsAuthorized(principal, IsValid(userItem), item, resourceItem, this.DefaultResourceActionItemAccessLevel);
				case ResourceItemActionPolicy.UseActionsWithFilter:
					item = GetResourceItem(resourceItem.Resource, resourceItem.Action, resourceItem.SubAction, true);
					return IsAuthorized(principal, IsValid(userItem), item, resourceItem, this.DefaultResourceActionItemAccessLevel);
				case ResourceItemActionPolicy.UseItems:
					item = await GetResourceItemAsync(resourceItem.Resource, resourceItem.Action, resourceItem.SubAction, resourceItem.ResourceItemId).ConfigureAwait(false);
					return IsAuthorized(principal, IsValid(userItem), item, resourceItem, this.DefaultResourceActionItemAccessLevel);
				case ResourceItemActionPolicy.UseItemsAndActions:
					item = await GetResourceItemAsync(resourceItem.Resource, resourceItem.Action, resourceItem.SubAction, resourceItem.ResourceItemId).ConfigureAwait(false);
					if (item != null)
					{
						if (IsAuthorized(principal, IsValid(userItem), item, resourceItem, this.DefaultResourceActionItemAccessLevel))
							return true;
					}
					item = GetResourceItem(resourceItem.Resource, resourceItem.Action, resourceItem.SubAction, false);
					return IsAuthorized(principal, IsValid(userItem), item, resourceItem, this.DefaultResourceActionItemAccessLevel);
				case ResourceItemActionPolicy.UseItemsAndActionsWithFilter:
					item = await GetResourceItemAsync(resourceItem.Resource, resourceItem.Action, resourceItem.SubAction, resourceItem.ResourceItemId).ConfigureAwait(false);
					if (item != null)
					{
						if (IsAuthorized(principal, IsValid(userItem), item, resourceItem, this.DefaultResourceActionItemAccessLevel))
							return true;
					}
					item = GetResourceItem(resourceItem.Resource, resourceItem.Action, resourceItem.SubAction, true);
					return IsAuthorized(principal, IsValid(userItem), item, resourceItem, this.DefaultResourceActionItemAccessLevel);
				case ResourceItemActionPolicy.UseItemsOrActions:
					item = await GetResourceItemAsync(resourceItem.Resource, resourceItem.Action, resourceItem.SubAction, resourceItem.ResourceItemId).ConfigureAwait(false);
					if (item == null)
						item = GetResourceItem(resourceItem.Resource, resourceItem.Action, resourceItem.SubAction, false);
					return IsAuthorized(principal, IsValid(userItem), item, resourceItem, this.DefaultResourceActionItemAccessLevel);
				case ResourceItemActionPolicy.UseItemsOrActionsWithFilter:
					item = await GetResourceItemAsync(resourceItem.Resource, resourceItem.Action, resourceItem.SubAction, resourceItem.ResourceItemId).ConfigureAwait(false);
					if (item == null)
						item = GetResourceItem(resourceItem.Resource, resourceItem.Action, resourceItem.SubAction, true);
					return IsAuthorized(principal, IsValid(userItem), item, resourceItem, this.DefaultResourceActionItemAccessLevel);
				default:
					return false;
			}
		}

		#endregion
		#region SaveUserItemAsync

		public async Task<Sidvall.Security.UserItem> SaveUserItemAsync(Sidvall.Security.UserItem item)
		{
			if (item == null)
				return null;
			var userItem = await GetUserItemAsync(null, item, false, item.Name).ConfigureAwait(false);
			AddUserItemToCache(userItem);
			return item;
		}

		#endregion

		#endregion
		#region Private Members

		#region AddUserItemToCache

		private void AddUserItemToCache(Sidvall.Security.UserItem userItem)
		{
			if (!this.CacheUserItems)
				return;
			if (userItem == null)
				return;
			if (string.IsNullOrWhiteSpace(userItem.Name))
				return;
			if (this.UserItems == null)
				return;
			else if (this.UserItems.Contains(userItem.Name))
				return;
			this.UserItems.Add(userItem);
		}

		#endregion
		#region AddUserItemsToCache

		private void AddUserItemsToCache(IEnumerable<IAuthorizationScopeUser> items)
		{
			if (!this.CacheUserItems)
				return;
			if (items == null)
				return;
			foreach (var item in items)
			{
				var userItem = GetUserItem(null, item);
				AddUserItemToCache(userItem);
			}
		}

		#endregion
		#region GetUserItemFromCache

		private Sidvall.Security.UserItem GetUserItemFromCache(IPrincipal principal)
		{
			var name = Sidvall.SystemContext.Current.PrincipalManager.GetIdentityName(principal);
			var userItem = GetUserItemFromCache(name, null);
			AddClaimsToPrincipal(principal, userItem);
			return userItem;
		}
		private Sidvall.Security.UserItem GetUserItemFromCache(string name, UserValidationResult userValidationResult)
		{
			if (!this.CacheUserItems)
				return null;
			if (string.IsNullOrWhiteSpace(name))
				return null;
			if (this.UserItems == null)
				return null;
			if (!this.UserItems.Contains(name))
			{
				switch (this.MissingUserPolicy)
				{
					case MissingUserPolicy.CreateInactiveUser:
						return GetUserItem(userValidationResult, name, this.DefaultUserContext, System.DateTime.UtcNow, System.DateTime.UtcNow, false, null);
					case MissingUserPolicy.CreateActiveUser:
						return GetUserItem(userValidationResult, name, this.DefaultUserContext, System.DateTime.UtcNow, System.DateTime.UtcNow, true, null);
					default:
						return null;
				}
			}
			return this.UserItems[name];
		}

		#endregion

		#region AddClaimsToPrincipal

		private static void AddClaimsToPrincipal(IPrincipal principal, Sidvall.Security.UserItem userItem)
		{
			if (userItem == null)
				return;
			AuthorizationScopeItemManager.AddClaimsToPrincipal(principal, userItem.Claims, Sidvall.AuthorizationManager.Security.ClaimTypes.HasApplicationClaims);
		}

		#endregion
		#region GetOrCreateAuthorizationScopeUserAsync

		private async Task<AuthorizationScopeUser> GetOrCreateAuthorizationScopeUserAsync(string name, Sidvall.Security.UserItem item, bool updateAccessTime)
		{
			if (string.IsNullOrWhiteSpace(name))
				return null;

			var authorizationScopeUser = await Sidvall.AuthorizationManager.SystemContext.Current.DataPortal.AuthorizationScopeUserManager.GetItemByPrimaryKeyAsync(this.AuthorizationScopeId, name).ConfigureAwait(false);
			if (authorizationScopeUser == null)
			{
				if (item != null)
				{
					authorizationScopeUser = new Net.Services.V1.AuthorizationScopeUser()
					{
						AuthorizationScopeId = this.AuthorizationScopeId,
						UserId = name,
						CreatedTime = System.DateTime.UtcNow,
						UpdatedTime = System.DateTime.UtcNow,
					};
				}
				else
				{
					switch (this.MissingUserPolicy)
					{
						case MissingUserPolicy.CreateInactiveUser:
							authorizationScopeUser = new Net.Services.V1.AuthorizationScopeUser()
							{
								AuthorizationScopeId = this.AuthorizationScopeId,
								UserId = name,
								UserContext = this.DefaultUserContext,
								UserClaims = string.Empty,
								CreatedTime = System.DateTime.UtcNow,
								UpdatedTime = System.DateTime.UtcNow,
								IsActive = false,
							};
							break;
						case MissingUserPolicy.CreateActiveUser:
							authorizationScopeUser = new Net.Services.V1.AuthorizationScopeUser()
							{
								AuthorizationScopeId = this.AuthorizationScopeId,
								UserId = name,
								UserContext = this.DefaultUserContext,
								UserClaims = string.Empty,
								CreatedTime = System.DateTime.UtcNow,
								UpdatedTime = System.DateTime.UtcNow,
								IsActive = true,
							};
							break;
						default:
							break;
					}
				}
			}
			if (authorizationScopeUser != null)
			{
				if (item != null)
				{
					authorizationScopeUser.UserContext = item.Context;
					if ((item.Claims != null) && (item.Claims.Count > 0))
						authorizationScopeUser.UserClaims = Sidvall.Serialization.SerializerManager.Current.SerializeObject(item.Claims);
					else
						authorizationScopeUser.UserClaims = string.Empty;
				}
				if (updateAccessTime)
					authorizationScopeUser.AccessTime = System.DateTime.UtcNow;
				authorizationScopeUser = await authorizationScopeUser.SaveItemAsync().ConfigureAwait(false);
			}
			return authorizationScopeUser;
		}

		#endregion
		#region GetResourceItem

		private ResourceItem GetResourceItem(string resource, string action, string subAction, bool filterResourceActionItems)
		{
			if (this.ResourceActions == null)
				return null;
			return this.ResourceActions.GetResourceItem(resource, action, subAction, filterResourceActionItems);
		}
		private ResourceItem GetResourceItem(string resource, string action, string subAction, string resourceItemId)
		{
			if (this.ResourceItemActions == null)
				return null;
			return this.ResourceItemActions.GetResourceItem(resource, action, subAction, resourceItemId);
		}

		#endregion
		#region GetResourceItemAsync

		private async Task<ResourceItem> GetResourceItemAsync(string resource, string action, string subAction, string resourceItemId)
		{
			Sidvall.Data.FetchListCriteria<Fields.AuthorizationScopeResourceItemField, Relations.AuthorizationScopeResourceItemRelation> criteria;
			AuthorizationScopeResourceItemCollectionItem items;
			ResourceItemCollection resourceItems;

			if (string.IsNullOrWhiteSpace(resource))
				return null;
			if (string.IsNullOrWhiteSpace(resourceItemId))
				return null;
			if (this.CacheResourceItemActions)
			{
				return GetResourceItem(resource, action, subAction, resourceItemId);
			}
			else
			{
				criteria = new Sidvall.Data.FetchListCriteria<Fields.AuthorizationScopeResourceItemField, Relations.AuthorizationScopeResourceItemRelation>();
				criteria.IncludedEntities = new Sidvall.Data.RelationCollection<Relations.AuthorizationScopeResourceItemRelation>();
				criteria.IncludedEntities.Add(Relations.AuthorizationScopeResourceItemRelation.AuthorizationScopeResourceItemClaims);
				items = await Sidvall.AuthorizationManager.SystemContext.Current.DataPortal.AuthorizationScopeResourceItemManager.GetItemsByAuthorizationScopeIdResourceResourceItemIdAsync(this.AuthorizationScopeId,
					resource, resourceItemId, criteria, null).ConfigureAwait(false);
				resourceItems = new ResourceItemCollection();
				foreach (var item in items.Items)
				{
					if (item.IsEnabled)
						resourceItems.Load(item.AccessLevel, ClaimManager.GetClaims(item.AuthorizationScopeResourceItemClaims.Items), true, true, item.Actions, item.SubActions);
				}
				var orderedResourceItems = (from o in resourceItems
											orderby o.Priority
											select o);
				return AuthorizationScopeItemManager.GetResourceItem(orderedResourceItems, action, subAction, false);
			}
		}

		#endregion
		#region GetResourceItemAccessLevel

		private ResourceItemAccessLevel GetResourceItemAccessLevel(ResourceItem resourceItem)
		{
			switch (resourceItem.AccessLevel)
			{
				case Sidvall.AuthorizationManager.Data.ResourceAccessLevel.Authenticated:
					return ResourceItemAccessLevel.Protected;
				case Sidvall.AuthorizationManager.Data.ResourceAccessLevel.AuthenticatedAndClaims:
				case Sidvall.AuthorizationManager.Data.ResourceAccessLevel.Claims:
					if (resourceItem.ClaimItemIndex.Count > 0)
						return ResourceItemAccessLevel.Protected;
					return ResourceItemAccessLevel.None;
				case Sidvall.AuthorizationManager.Data.ResourceAccessLevel.Public:
					return ResourceItemAccessLevel.Public;
				default:
					return ResourceItemAccessLevel.None;
			}
		}

		#endregion
		#region GetUserItem

		private Sidvall.Security.UserItem GetUserItem(UserValidationResult userValidationResult, IAuthorizationScopeUser authorizationScopeUser)
		{
			return GetUserItem(userValidationResult, authorizationScopeUser.UserId, authorizationScopeUser.UserContext, authorizationScopeUser.CreatedTime,
				authorizationScopeUser.UpdatedTime, authorizationScopeUser.IsActive, authorizationScopeUser.UserClaims);
		}
		private Sidvall.Security.UserItem GetUserItem(UserValidationResult userValidationResult, string name, string context, System.DateTime createdTime,
			System.DateTime updatedTime, bool isActive, string userClaims)
		{
			var userItem = new Sidvall.Security.UserItem()
			{
				Name = name,
				Context = context,
				CreatedTime = createdTime,
				UpdatedTime = updatedTime,
				UserStatus = Sidvall.Security.UserStatus.Valid,
			};
			if ((this.RequireActiveUser) && (!isActive))
				userItem.UserStatus = Sidvall.Security.UserStatus.InvalidUser;
			else if (userValidationResult != null)
				userItem.UserStatus = userValidationResult.UserStatus;
			if (!string.IsNullOrWhiteSpace(userClaims))
			{
				var claims = Sidvall.Security.ClaimCollection.Deserialize(userClaims);
				userItem.Claims.AddRange(claims);
			}
			if (userValidationResult != null)
				userItem.Claims.AddRange(userValidationResult.Claims);
			return userItem;
		}

		#endregion
		#region GetUserItemAsync

		private async Task<Sidvall.Security.UserItem> GetUserItemAsync(UserValidationResult userValidationResult, Sidvall.Security.UserItem item, bool updateAccessTime, IPrincipal principal)
		{
			var name = Sidvall.SystemContext.Current.PrincipalManager.GetIdentityName(principal);
			var userItem = await GetUserItemAsync(userValidationResult, item, updateAccessTime, name).ConfigureAwait(false);
			AddClaimsToPrincipal(principal, userItem);
			return userItem;
		}
		private async Task<Sidvall.Security.UserItem> GetUserItemAsync(UserValidationResult userValidationResult, Sidvall.Security.UserItem item, bool updateAccessTime, string name)
		{
			if (this.CacheUserItems)
				return GetUserItemFromCache(name, userValidationResult);
			var authorizationScopeUser = await GetOrCreateAuthorizationScopeUserAsync(name, item, updateAccessTime).ConfigureAwait(false);
			return GetUserItem(userValidationResult, authorizationScopeUser);
		}

		#endregion
		#region IsValid

		private bool IsValid(Sidvall.Security.UserItem item)
		{
			if (item != null)
			{
				if (item.UserStatus == Sidvall.Security.UserStatus.Valid)
					return true;
			}
			return !this.RequireActiveUser;
		}

		#endregion
		#region Load

		internal void Load(IAuthorizationScope item, bool includeResourceItems, bool includeUserItems)
		{
			if (item == null)
				return;
			this.AuthorizationScopeId = item.AuthorizationScopeId.Value;
			this.DefaultResourceActionAccessLevel = item.DefaultResourceActionAccessLevel;
			this.DefaultResourceActionItemAccessLevel = item.DefaultResourceItemAccessLevel;
			this.ResourceItemActionPolicy = item.ResourceItemPolicyId;
			this.ResourceActions = new ResourceItemCollectionDictionary(System.StringComparer.OrdinalIgnoreCase);
			this.ResourceActions.Load(item.AuthorizationScopeResourceActions);
			this.CacheResourceItemActions = includeResourceItems;
			this.CacheUserItems = includeUserItems;
			this.MissingUserPolicy = item.MissingUserPolicyId;
			this.DefaultUserContext = item.DefaultUserContext;
			this.RequireActiveUser = item.RequireActiveUser;
			if (includeResourceItems)
			{
				this.ResourceItemActions = new ResourceItemCollectionDictionary(System.StringComparer.OrdinalIgnoreCase);
				this.ResourceItemActions.Load(item.AuthorizationScopeResourceItems);
			}
			if (includeUserItems)
			{
				this.UserItems = new Sidvall.Security.UserItemCollection();
				AddUserItemsToCache(item.AuthorizationScopeUsers);
			}
		}

		#endregion

		#endregion
	}
}
